// ==UserScript==
// @name     Invidious Keyboard Browse
// @version  1
// @grant    none
// @include  https://yewtu.be/channel/*
// @include  https://yewtu.be/watch*
// @include  https://yewtu.be/search*
// @include  https://yewtu.be/feed/*
// @include  https://vid.puffyan.us/channel/*
// @include  https://vid.puffyan.us/watch*
// @include  https://vid.puffyan.us/search*
// @include  https://vid.puffyan.us/feed/*
// @include  https://inv.riverside.rocks/channel/*
// @include  https://inv.riverside.rocks/watch*
// @include  https://inv.riverside.rocks/search*
// @include  https://inv.riverside.rocks/feed/*
// @include  https://invidious.kavin.rocks/channel/*
// @include  https://invidious.kavin.rocks/watch*
// @include  https://invidious.kavin.rocks/search*
// @include  https://invidious.kavin.rocks/feed/*
// @include  https://invidio.xamh.de/channel/*
// @include  https://invidio.xamh.de/watch*
// @include  https://invidio.xamh.de/search*
// @include  https://invidio.xamh.de/feed/*
// @include  https://y.com.sb/channel/*
// @include  https://y.com.sb/watch*
// @include  https://y.com.sb/search*
// @include  https://y.com.sb/feed/*
// @include  https://invidious.nerdvpn.de/channel/*
// @include  https://invidious.nerdvpn.de/watch*
// @include  https://invidious.nerdvpn.de/search*
// @include  https://invidious.nerdvpn.de/feed/*
// @include  https://yt.artemislena.eu/channel/*
// @include  https://yt.artemislena.eu/watch*
// @include  https://yt.artemislena.eu/search*
// @include  https://yt.artemislena.eu/feed/*
// @include  https://invidious.tiekoetter.com/channel/*
// @include  https://invidious.tiekoetter.com/watch*
// @include  https://invidious.tiekoetter.com/search*
// @include  https://invidious.tiekoetter.com/feed/*
// @include  https://invidious.flokinet.to/channel/*
// @include  https://invidious.flokinet.to/watch*
// @include  https://invidious.flokinet.to/search*
// @include  https://invidious.flokinet.to/feed/*
// @include  https://inv.bp.projectsegfau.lt/channel/*
// @include  https://inv.bp.projectsegfau.lt/watch*
// @include  https://inv.bp.projectsegfau.lt/search*
// @include  https://inv.bp.projectsegfau.lt/feed/*
// @include  https://inv.vern.cc/channel/*
// @include  https://inv.vern.cc/watch*
// @include  https://inv.vern.cc/search*
// @include  https://inv.vern.cc/feed/*
// @include  https://inv.odyssey346.dev/channel/*
// @include  https://inv.odyssey346.dev/watch*
// @include  https://inv.odyssey346.dev/search*
// @include  https://inv.odyssey346.dev/feed/*
// @include  https://invidious.snopyta.org/channel/*
// @include  https://invidious.snopyta.org/watch*
// @include  https://invidious.snopyta.org/search*
// @include  https://invidious.snopyta.org/feed/*
// @include  https://invidious.baczek.me/channel/*
// @include  https://invidious.baczek.me/watch*
// @include  https://invidious.baczek.me/search*
// @include  https://invidious.baczek.me/feed/*
// @include  https://invidious.sethforprivacy.com/channel/*
// @include  https://invidious.sethforprivacy.com/watch*
// @include  https://invidious.sethforprivacy.com/search*
// @include  https://invidious.sethforprivacy.com/feed/*
// @include  https://yt.funami.tech/channel/*
// @include  https://yt.funami.tech/watch*
// @include  https://yt.funami.tech/search*
// @include  https://yt.funami.tech/feed/*
// @include  https://invidious.drivet.xyz/channel/*
// @include  https://invidious.drivet.xyz/watch*
// @include  https://invidious.drivet.xyz/search*
// @include  https://invidious.drivet.xyz/feed/*
// @include  https://vid.priv.au/channel/*
// @include  https://vid.priv.au/watch*
// @include  https://vid.priv.au/search*
// @include  https://vid.priv.au/feed/*
// @include  https://invidious.silur.me/channel/*
// @include  https://invidious.silur.me/watch*
// @include  https://invidious.silur.me/search*
// @include  https://invidious.silur.me/feed/*
// @include  https://invidious.epicsite.xyz/channel/*
// @include  https://invidious.epicsite.xyz/watch*
// @include  https://invidious.epicsite.xyz/search*
// @include  https://invidious.epicsite.xyz/feed/*
// @include  https://invidious.slipfox.xyz/channel/*
// @include  https://invidious.slipfox.xyz/watch*
// @include  https://invidious.slipfox.xyz/search*
// @include  https://invidious.slipfox.xyz/feed/*
// @include  https://iv.ggtyler.dev/channel/*
// @include  https://iv.ggtyler.dev/watch*
// @include  https://iv.ggtyler.dev/search*
// @include  https://iv.ggtyler.dev/feed/*
// @include  https://invidious.dhusch.de/channel/*
// @include  https://invidious.dhusch.de/watch*
// @include  https://invidious.dhusch.de/search*
// @include  https://invidious.dhusch.de/feed/*
// @include  https://invidious.weblibre.org/channel/*
// @include  https://invidious.weblibre.org/watch*
// @include  https://invidious.weblibre.org/search*
// @include  https://invidious.weblibre.org/feed/*
// @include  https://invidious.esmailelbob.xyz/channel/*
// @include  https://invidious.esmailelbob.xyz/watch*
// @include  https://invidious.esmailelbob.xyz/search*
// @include  https://invidious.esmailelbob.xyz/feed/*
// @include  https://iv.melmac.space/channel/*
// @include  https://iv.melmac.space/watch*
// @include  https://iv.melmac.space/search*
// @include  https://iv.melmac.space/feed/*
// @include  https://invidious.privacydev.net/channel/*
// @include  https://invidious.privacydev.net/watch*
// @include  https://invidious.privacydev.net/search*
// @include  https://invidious.privacydev.net/feed/*
// @include  https://invidious.lidarshield.cloud/channel/*
// @include  https://invidious.lidarshield.cloud/watch*
// @include  https://invidious.lidarshield.cloud/search*
// @include  https://invidious.lidarshield.cloud/feed/*
// @include  https://invidious.namazso.eu/channel/*
// @include  https://invidious.namazso.eu/watch*
// @include  https://invidious.namazso.eu/search*
// @include  https://invidious.namazso.eu/feed/*
// ==/UserScript==


/* Browse video's on Invidious with the keyboard
 *
 * Shortcuts:
 *   SHIFT + arrow-up      Go up a video
 *   SHIFT + arrow-down    Go down a video
 *   SHIFT + arrow-left    Go one video to the left
 *   SHIFT + arrow-right   Go one video to the right
 *   ENTER                 Follow link of selected video
 *   ESCAPE                De-select selected video
 *
 * When no video is selected yet it will select the first visible video, when
 * no video is visible it will select the first video. (On usage of
 * SHIFT + arrow)*/


const KEY_UP    = 38;
const KEY_RIGHT = 39;
const KEY_DOWN  = 40;
const KEY_LEFT  = 37;
const KEY_ENTER = 13;
const KEY_ESC   = 27;

const UP    = 1;
const RIGHT = 2;
const DOWN  = 3;
const LEFT  = 4;

var SELECTED_ELEM = null;


function itemIsVisible(item) {
  if ((item.offsetTop - window.pageYOffset) < 0 || (item.offsetTop + item.clientHeight) > (window.innerHeight + window.pageYOffset))  {
    return false; 
  }
  return true;
}


function* getWantedElements() {
  const thumbnailElems = document.getElementsByClassName("thumbnail");
  for (const item of thumbnailElems) {
    if (item.parentNode.tagName !== "A") {
      continue; 
    }
    yield item;
  }
  return [];
}


function* getVisibleElements() {
  for (const item of getWantedElements()) {
    if (itemIsVisible(item) === false) {
      continue; 
    }
    yield item;
  }
}


function* getRowElements() {
  curOffsetTop = SELECTED_ELEM.offsetTop;
  for (const item of getVisibleElements()) {
    if (item.offsetTop !== curOffsetTop) {
      continue; 
    }
    yield item;
  }
}


function* getColumnElements() {
  currOffsetLeft = SELECTED_ELEM.offsetLeft;
  for (const item of getWantedElements()) {
    if (item.offsetLeft !== currOffsetLeft) {
      continue; 
    }
    yield item;
  }
}


function getDownElement() {
  currOffsetTop = SELECTED_ELEM.offsetTop;
  for (const item of getColumnElements()) {
    if (item.offsetTop > currOffsetTop) {
      return item; 
    }
  }
  return null;
}


function getUpElement() {
  prevItem = null;
  for (const item of getColumnElements()) {
    if (item === SELECTED_ELEM) {
      return prevItem; 
    }
    prevItem = item;
  }
  return null;
}


function getRightElement() {
  curOffsetLeft = SELECTED_ELEM.offsetLeft;
  for (const item of getRowElements()) {
    if (item.offsetLeft > curOffsetLeft) {
      return item; 
    }
  }
  return null;
}


function getLeftElement() {
  prevItem = null;
  for (const item of getRowElements()) {
    if (item === SELECTED_ELEM) {
      return prevItem; 
    }
    prevItem = item;
  }
  return null;
}


function setSelected(item) {
  item.focus();
  item.style.border = "2px solid red";
  if (itemIsVisible(item) == false) {
    item.scrollIntoView();
  }
  SELECTED_ELEM = item;
}


function clearSelection() {
  SELECTED_ELEM.style.border = "none";
  SELECTED_ELEM = null;
}


function mayProcess() {
  let activeElem = document.activeElement;
  if (activeElem !== document.body) {
    if (activeElem.tagName === "INPUT") {
      return false; 
    }
    if (activeElem.tagName === "VIDEO") {
      return false; 
    }
  }
  return true;
}


window.onkeydown = function keydown(event) {
  let keyAction = null;

  if (mayProcess() === false) {
    return; 
  }

  // remove potential selection
  document.getSelection().removeAllRanges();

  if (event.keyCode === KEY_ESC) { // escape
    if (SELECTED_ELEM !== null) {
      clearSelection();
    }
    return;
  }
  else if (event.keyCode === KEY_ENTER) { // enter
    if (SELECTED_ELEM === null) {
      return; 
    }
    const item = SELECTED_ELEM;
    clearSelection();
    // Make sure document.body is the active element, else it may follow the
    // active element.
    if (document.activeElement !== document.body) {
      document.activeElement.blur();
    }
    item.parentNode.click();
    return;
  }

  // SHIFT is our modifier key
  if (event.shiftKey === false) {
   	return; 
  }

  if (event.keyCode === KEY_LEFT) { // arrow-left
    keyAction = LEFT;
  }
  else if (event.keyCode === KEY_UP) { // arrow-up
    keyAction = UP;
  }
  else if (event.keyCode === KEY_RIGHT) { // arrow-right
    keyAction = RIGHT;
  }
  else if (event.keyCode === KEY_DOWN) { // arrow-down
    keyAction = DOWN;
  }
  else {
    return;
  }

  if (SELECTED_ELEM !== null) {
    if (itemIsVisible(SELECTED_ELEM) === false) {
      // clear previous selected
      clearSelection();
    }
  }

  if (SELECTED_ELEM === null) {
    // None selected yet, no matter the action, just set the first
    for (const item of getVisibleElements()) {
      setSelected(item);
      return; 
    }
    // No items visible, select first item
    for (const item of getWantedElements()) {
      setSelected(item);
      break; 
    }
    return;
  }

  if (keyAction === RIGHT) {
    let rightItem = getRightElement();
    if (rightItem !== null) {
      clearSelection();
      setSelected(rightItem);
    }
  }

  else if (keyAction === LEFT) {
    let item = getLeftElement();
    if (item !== null) {
      clearSelection();
      setSelected(item);
    }
  }

  else if (keyAction === DOWN) {
    let item = getDownElement();
    if (item !== null) {
      clearSelection();
      setSelected(item);
    }
  }

  else if (keyAction === UP) {
    let item = getUpElement();
    if (item !== null) {
      clearSelection();
      setSelected(item);
    }
  }
}
